package com.example.verification.demo.interceptor;

import com.example.verification.demo.common.util.Constants;
import com.example.verification.demo.common.util.JacksonUtils;
import com.example.verification.demo.common.util.MD5Utils;
import com.example.verification.demo.merchant.repository.IMerchantRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.*;
import java.lang.reflect.Method;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;

import com.example.verification.demo.common.exception.*;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Created by hzy on 2018/9/7.
 * API接口校验拦截器.
 *
 * 校验模式类似于微信支付。
 */

public class ApiVerificationInterceptor implements HandlerInterceptor {
    private static final Logger LOGGER = LoggerFactory.getLogger(ApiVerificationInterceptor.class);
    private static final String KEY_MCH_ID = "mch_id";         // 商户id在请求参数中的名称
    private static final String SIGN_STR_PARAM_NAME = "sign";  // 请求信息中保存sign信息的 header name 。

//    // 请求提供的格式
//    private static final boolean IS_XML_FORMAT_BODY = false;

    @Resource
    IMerchantRepository merchantRepository;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 如果不是映射到方法则直接通过。(SpringMVC中处理请求的方法叫做HandlerMethod)
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        /*
         * 不拦截 BasicErrorController 所处理的 404,500 等错误的响应
         * 如果项目中有自定义的 ErrorController 那么就无需下面这一个判断处理逻辑。
         */
        if ("BasicErrorController".equals(method.getDeclaringClass().getSimpleName())){
            return true;
        }

        //判断方法是否被 @IgnoreApiVerify 修饰
        IgnoreApiVerify methodAnnotation = method.getAnnotation(IgnoreApiVerify.class);
        //判断方法所在的Controller有没有被 IgnoreApiVerify.class 修饰
        IgnoreApiVerify classAnnotation  = method.getDeclaringClass().getAnnotation(IgnoreApiVerify.class);

        // 没有 @IgnoreApiVerify 注解,则需要认证
        if (methodAnnotation == null && classAnnotation == null) {
            String signStr = request.getHeader(SIGN_STR_PARAM_NAME);
            if (StringUtils.isEmpty(signStr)) {
                LOGGER.info("preHandle: intercepted! req url:[{}] , response coed: [{}] ",request.getRequestURL(),response.getStatus());
                //
                responseVerificationFailed();
                return false;// 拦截
            }

            if (verifySignStr(signStr,request)){
                return true;
            }else {
                responseVerificationFailed();
                return false;
            }
        } else {
            //所在方法或者类上有@IgnoreLogin注解,则不拦截
            return true;
        }
    }

    private boolean verifySignStr(String signStr, HttpServletRequest request) {
        MimeType mimeType = MimeType.valueOf(request.getContentType());
        if (MimeTypeUtils.APPLICATION_JSON.equals(mimeType)){
            return verifyBody(signStr,request,true);
        }else if (MimeTypeUtils.APPLICATION_XML.equals(mimeType)){
            return verifyBody(signStr,request,false);
        }else {
            LOGGER.info("verifySignStr: Not supported ContentType:[{}]",request.getContentType());
            responseVerificationFailed();
        }
        return false;
    }

    private boolean verifyBody(String signStr, HttpServletRequest request, boolean isJsonFromatBody) {
        if (StringUtils.isEmpty(signStr)){
            LOGGER.info("verifyBody: Parameter [signStr] can not be empty! Url is: [{}]",request.getRequestURL());
            return false;
        }

        Map<String, Object> stringObjectMap = parseRequestBody(request, isJsonFromatBody);
        if (stringObjectMap == null || stringObjectMap.isEmpty()){
            LOGGER.info("verifyBody: Request body parse failed! Url is: [{}]",request.getRequestURL());
            return false;
        }

        String mchId = (String) stringObjectMap.get(KEY_MCH_ID);
        if (StringUtils.isEmpty(mchId)){
            LOGGER.info("verifyBody: Parameter [mch_id] can not be empty ! Url is: [{}]",request.getRequestURL());
            return false;
        }


        String key = merchantRepository.findKeyByMerchantId(mchId);
        if (StringUtils.isEmpty(key)){
            LOGGER.info("verifyBody: Parameter [mch_id]:[{}] not invalid ! Url is: [{}]",mchId,request.getRequestURL());
            return false;
        }

        //
        return signStr.equals(genSignatureStr(stringObjectMap, key));
    }

    private boolean verifyFromXmlBody(String signStr,HttpServletRequest request) {
        return false;
    }

    /**
     * Json 或者 xml 格式的请求体转化成 map
     */
    private Map<String, Object> parseRequestBody(HttpServletRequest request, boolean isJsonFormatBody) {
        BufferedReader reader = null;
        try {
            //
            reader = request.getReader();
            StringBuilder builder = new StringBuilder();

            String line;
            while((line = reader.readLine()) != null) {
                builder.append(line);
            }
            
            String bodyString = builder.toString();
            if (isJsonFormatBody){
                return JacksonUtils.json2map(bodyString);
            }else {
                return xmlToMap(bodyString);
            }
        } catch (Exception e) {
            LOGGER.error("parseRequestBody: ", e);
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (IOException e) {
                LOGGER.error("parseRequestBody: ", e);
            }
        }
        return new HashMap<>();
    }

    /**
     * 提示客户端,接口校验失败。
     */
    private void responseVerificationFailed() {
        throw new MyException(Constants.RespCode.VERIFICATION_FAILED, Constants.RespMsg.VERIFICATION_FAILED);
    }

    /**
     * 根据请求体的内容与用户的秘钥生成应该签名字符串
     * @param map : 请求体转换后生成的map
     * @param key : 用户的秘钥。
     */
    private String genSignatureStr(Map<String, Object> map, String key) {
        StringBuilder sb = new StringBuilder();

        //1. 参数名ASCII码从小到大排序
        //2. 如果参数的值为空不参与签名
        //3. KEY-value pair --> "KEY=value"
        map.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey))
                .filter(stringObjectEntry -> stringObjectEntry.getValue() != null)
                .map(stringObjectEntry -> stringObjectEntry.getKey() + "=" + stringObjectEntry.getValue())
                .forEach(s -> sb.append(s).append("&"));

        //4. 拼接API密钥
        String stringA = sb.toString();
        String stringSignTemp = stringA + "key=" + key;
        //
//        LOGGER.info("genSignatureStr: stringSignTemp:[{}]",stringSignTemp);
        //5. MD5 --> toUpperCase
        return MD5Utils.getEncodeMD5String(stringSignTemp).toUpperCase();
    }

    /**
     * XML格式字符串转换为Map
     *
     * @param xml XML字符串
     * @return XML数据转换后的Map
     * @throws Exception
     */
    public static Map<String, Object> xmlToMap(String xml) {
        try {
            Map<String, Object> data = new HashMap<>();
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            InputStream stream = new ByteArrayInputStream(xml.getBytes("UTF-8"));
            org.w3c.dom.Document doc = documentBuilder.parse(stream);
            doc.getDocumentElement().normalize();
            NodeList nodeList = doc.getDocumentElement().getChildNodes();
            for (int idx = 0; idx < nodeList.getLength(); ++idx) {
                Node node = nodeList.item(idx);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    org.w3c.dom.Element element = (org.w3c.dom.Element) node;
                    data.put(element.getNodeName(), element.getTextContent());
                }
            }
            stream.close();
            return data;
        } catch (Exception e) {
            LOGGER.error("xmlToMap: ", e);
            return null;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}